#region (c) 2014 THN Solutions LLC. - All rights reserved
/*
Simplified BSD License (BSD)
Copyright (c) 2014, THN Solutions LLC. ( www.ThnSolutions.com )
Author: Nguyễn, M. Hải                 ( www.Minh-Hai.com     )
All rights reserved.

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:

    * Redistributions of source code must retain the above copyright notice, this 
      list of conditions and the following disclaimer.

    * Redistributions in binary form must reproduce the above copyright notice, 
      this list of conditions and the following disclaimer in the documentation 
      and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
POSSIBILITY OF SUCH DAMAGE.
*/
#endregion

#region Record of Change
//             							CHANGE HISTORY
// -------------------------------------------------------------------------------------
// |   DATE    | DEVELOPER  | DESCRIPTION                                              |
// -------------------------------------------------------------------------------------
// | 11-Nov-09 | HaiNM      | First creation.                                          |
// -------------------------------------------------------------------------------------
//
#endregion

#region Record of Audit
//             							REVIEW HISTORY
// -------------------------------------------------------------------------------------
// |   DATE    | REVIEWER   | NOTES                                                    |
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
//
#endregion

#region Using directives
using System;
using System.Drawing;
using System.Windows.Forms;
using System.ComponentModel;

using Thn;
using System.Text;
using Thn.Interface.Utils;
#endregion

namespace Thn.Interface.Vcl
{
    /// <summary>
    /// A control for editing date values with a popup calendar.
    /// </summary>
    [ToolboxItem(true), DefaultProperty("Title"), DefaultEvent("DateChanged"), ToolboxBitmap(typeof(ResourceBeacon), "Thn.Interface.Vcl.Resources.Icons.DateBox.bmp")]
#if DEBUG
    public
#else
    public
#endif
 class DateBox : OldPopupBoxBase
    {
        #region Fields
        /// <summary>
        /// Turn this flag to true for suppress parse from 
        /// the input text to date time value when set value to Text property.
        /// </summary>
        protected bool mSuppressParsing = false;

        private char[] mDigits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
        private StringBuilder mInnerText = null;
        private int mTextSelectionPos = 0;

        int mYear;
        int mMonth;
        int mDay;
        int mMinYear;
        int mMaxYear;
        #endregion

        #region Properties

        #region Date
        private DateTime mDate = DateTime.Now;
        /// <summary>
        /// Gets or Sets date value of the control.
        /// </summary>
        [Browsable(true), Category("Value"), Description("Gets or sets date value.")]
        public DateTime Date
        {
            get { return mDate; }
            set 
            {
                if (mDate != value)
                {
                    DateTime old = mDate;
                    mDate = value;
                    DateToCaption();

                    OnValueChanged(old, mDate);
                    RaiseDateChanged(old, mDate);
                    RaiseValueChanged(EventArgs.Empty);
                }
                #region Defensive tracing
                else
                {
                    // do not process on the same value.
                }
                #endregion
            }
        }
        #endregion  

        #region Text
        /// <summary>
        /// Override this property for allow set a date string to its value by code.
        /// </summary>
        [Browsable(false), EditorBrowsable(EditorBrowsableState.Never), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        public override string Text
        {
            get { return base.Text; }
            set
            {
                if (Text != value)
                {
                    // backup old value
                    DateTime old = mDate;

                    Parse();

                    OnValueChanged(old, mDate);
                    RaiseDateChanged(old, mDate);
                    RaiseValueChanged(EventArgs.Empty);
                }
                #region Defensive tracing
                else
                {
                    // do not process on the same value.
                }
                #endregion
            }
        }
        #endregion

        #region Alignment
        /// <summary>
        /// Gets or Sets alignment of content.
        /// </summary>
        [Browsable(true), DefaultValue(ContentAlignment.MiddleCenter), Category("Appearance"), Description("How to arrange contents")]
        public ContentAlignment Alignment
        {
            get { return RenderContext.Alignment; }
            set
            {
                RenderContext.Alignment = value;
                InvalidateLayout();
            }
        }
        #endregion

        #region Text Style
        /// <summary>
        /// Gets/Sets font style for rendering text.
        /// </summary>
        [Browsable(true), DefaultValue(TextStyle.Normal), Category("Appearance"), Description("Font style for rendering text")]
        public TextStyle TextStyle
        {
            get { return RenderContext.TextStyle; }
            set
            {
                RenderContext.TextStyle = value;
                Invalidate();
            }
        }
        #endregion

        #region Is Show Full Date Time
        bool mIsShowFullDateTime = false;
        /// <summary>
        /// Gets or Sets value determine whether the date time is show
        /// full value including date and time or not.
        /// <para>Note:</para>
        /// <para>This param just active in read only mode.</para>
        /// <para>Default value if false.</para>
        /// </summary>
        [Browsable(true), DefaultValue(false), Category("Appearance"), Description("In ReadOnly mode the text value will be show under short mode or full mode.")]
        public bool IsShowFullDateTime
        {
            get { return mIsShowFullDateTime; }
            set 
            {
                mIsShowFullDateTime = value;
                DateToCaption();
                InvalidateLayout();
            }
        }
        #endregion  

        #endregion

        #region Events

        #region Date Changed
        /// <summary>
        /// This event is raised after date time value is changed.
        /// </summary>
        public event DateTimeValueChangedHandler DateChanged;
        /// <summary>
        /// Invoke event DateChanged.
        /// </summary>
        protected void RaiseDateChanged(DateTime oldValue, DateTime newValue)
        {
            DateTimeValueChangedHandler handler = DateChanged;
            if (handler != null) handler(this, oldValue, newValue);
        }
        #endregion

        #endregion

        #region Set Date
        /// <summary>
        /// Set value for property Date without causing event DateChanged to be fired.
        /// </summary>
        public void SetDate(DateTime value)
        {
            DateTime previous = mDate;
            mDate = value;
            OnDateChanged(previous, mDate);
            Invalidate();
        }
        #endregion

        #region On Date Changed
        /// <summary>
        /// This method is invoked when property Date is modified.
        /// </summary>
        protected virtual void OnDateChanged(DateTime oldValue, DateTime newValue) { }
        #endregion

        #region Base methods

        #region On Popup Accepted
        /// <summary>
        /// This method is raised after the user closed popup editor and wish to accept modifications on editor
        /// </summary>
        /// <param name="popup">The controller which manages the popup</param>
        protected override void OnPopupAccepted(PopupController popup)
        {
            // TLN modified on 07Feb2010

            // old code
            //this.Date = (DateTime)popup.Data;

            // new code
            if (!this.ReadOnly)
            {
                this.Date = (DateTime)popup.Data;
            }
            #region Defensive tracing
            else
            {
                // Do not set value in read only mode.
            }
            #endregion
        }
        #endregion        

        #region On Show Popup
        /// <summary>
        /// This method is raised before a popup is shown. Override this method to assign the data that is going to be used by popup editor
        /// </summary>
        /// <param name="popup">The controller which manages the popup</param>
        protected override void OnShowPopup(PopupController popup)
        {
            popup.Data = Date;
        }
        #endregion        

        #region On Value Changed
        /// <summary>
        /// This method will be called after date time value is changed.
        /// </summary>
        protected virtual void OnValueChanged(DateTime oldValue, DateTime newValue) { }
        #endregion        

        #endregion

        #region Got/Lost Focused

        #region Notify On Lost Focus
        /// <summary>
        /// Override this method for parse value from input Text property
        /// to date time value when control is lost focus.
        /// </summary>
        internal override void NotifyInnerTextBoxOnLostFocus(EventArgs e)
        {
            base.NotifyInnerTextBoxOnLostFocus(e);
            Parse();
        }
        #endregion

        #region On Got Focus
        /// <summary>
        /// Override this method for process the inner text when the control got focus.
        /// </summary>
        void Inner_GotFocus(object sender, EventArgs e)
        {
            mInnerText.Remove(0, mInnerText.Length);
            mInnerText.Append(this.Text);
            mTextSelectionPos = this.SelectionStart;
        }
        #endregion

        #endregion

        #region Key Events

        #region Notify On Key Down
        /// <summary>
        /// Override this method for process when user click on the arrow keys.
        /// </summary>
        internal override void NotifyInnerTextBoxOnKeyDown(KeyEventArgs e)
        {
            switch (e.KeyCode)
            {
                case Keys.Left: BackWard(); break;
                case Keys.Right: Forward(); break;
                #region Defensive tracing
                default:
                    // not care key code, do nothing.
                    break;
                #endregion
            }

            e.Handled = true;
        }
        #endregion

        #region Notify On Key Press
        /// <summary>
        /// Override this method to handle the input character and its position.
        /// </summary>
        internal override void NotifyInnerTextBoxOnKeyPress(KeyPressEventArgs e)
        {
            StringBuilder tmp = new StringBuilder();
            tmp.Append(mInnerText.ToString());
            if (IsDigits(e.KeyChar))
            {
                tmp[mTextSelectionPos] = e.KeyChar;
                if (IsValidDate(tmp.ToString()))
                {
                    mInnerText = tmp;
                    Forward();
                }
                #region Defensive tracing
                else
                {
                    // invalid key, do nothing.
                }
                #endregion
            }
            #region Defensive tracing
            else 
            {
                // invalid key, do nothing.
            }
            #endregion

            e.Handled = true;
        }
        #endregion

        #region Notify On Key Up
        /// <summary>
        /// Override this method to correct the value after input.
        /// </summary>
        internal override void NotifyInnerTextBoxOnKeyUp(KeyEventArgs e)
        {
            mIsInputByHand = true;
            this.Text = mInnerText.ToString();
            this.SelectionStart = mTextSelectionPos;
            this.SelectionLength = 1;

            e.Handled = true;
        }
        #endregion

        #endregion

        #region Mouse Events

        #region Notify On Mouse Down
        /// <summary>
        /// Override this method for get the position of the text.
        /// </summary>
        internal override void NotifyInnerTextBoxOnMouseDown(MouseEventArgs e)
        {
            int pos = this.Inner.GetCharIndexFromPosition(e.Location);
            if (pos == 2 || pos == 5) pos++;
            mTextSelectionPos = pos;
        }
        #endregion

        #endregion

        #region Inner methods

        #region Parse
        /// <summary>
        /// Parse value from Text property to date time value.
        /// </summary>
        void Parse()
        {
            if ((!IsPopping) && (!Focused) && (!mSuppressParsing))
            {
                string input = mIsInputByHand ? mInnerText.ToString() : Inner.Text;
                //Log.Debug("Start - [{0}]", input);
                DateParser parser = new DateParser();

                try
                {
                    DateTime newValue = parser.Parse(input);
                    //mDate = newValue;
                    AssignDate(newValue);
                    //Log.Debug("Day: {0} Month: {1} Year: {2}", mDate.Day, mDate.Month, mDate.Year);
                }
                catch (Exception ex)
                {
                    //Log.Error(ex);
                    //AssignDate(Nulls.DateTime);
                }

                // turn of input by hand flag
                mIsInputByHand = false;
            }
        }
        #endregion

        #region Assign Value
        /// <summary>
        /// Set property date without causing looping parsing
        /// </summary>
        void AssignDate(DateTime value)
        {
            mSuppressParsing = true;
            mDate = value;
            DateToCaption();
            mSuppressParsing = false;
        }
        #endregion

        #region Date To Caption
        void DateToCaption()
        {
            string[] buf = new string[3] { "", "", "" };

            int dIdx = DateTimeHelper.GetOrder(DateElement.Day);
            int mIdx = DateTimeHelper.GetOrder(DateElement.Month);
            int yIdx = DateTimeHelper.GetOrder(DateElement.Year);

            buf[dIdx] = string.Format("{0}", NumberToString(mDate.Day));
            buf[mIdx] = string.Format("{0}", NumberToString(mDate.Month));
            buf[yIdx] = string.Format("{0}", NumberToString(mDate.Year));

            #region Normal case
            if (!this.IsShowFullDateTime)
            {
                base.Text = string.Format("{0}{1}{2}{3}{4}", buf[0], mDateSeperator, buf[1], mDateSeperator, buf[2]);
            }
            #endregion

            #region Special case
            else if (IsShowFullDateTime && ReadOnly)
            {
                base.Text = mDate.ToString();
            }
            #endregion

            #region Defensive tracing
            else
            {
                base.Text = string.Format("{0}{1}{2}{3}{4}", buf[0], mDateSeperator, buf[1], mDateSeperator, buf[2]);
            }
            #endregion
        }
        #endregion

        #region Process Input Character
        /// <summary>
        /// Determine whether the value to input to Text property by
        /// hand, type to text box in runtime mode, or by code, set from code.
        /// <para>This flag will be turn off within the Parse method.</para>
        /// </summary>
        bool mIsInputByHand = false;
        string mDateSeperator = UI.Culture.DateTimeFormat.DateSeparator;
        string mTimeSeperator = UI.Culture.DateTimeFormat.TimeSeparator;

        bool IsValidDate(string date)
        {
            string[] splits = date.Split(new string[] { mDateSeperator }, StringSplitOptions.RemoveEmptyEntries);
            if (splits.Length == 3)
            {
                string year, month, day;
                int dIdx = DateTimeHelper.GetOrder(DateElement.Day);
                int mIdx = DateTimeHelper.GetOrder(DateElement.Month);
                int yIdx = DateTimeHelper.GetOrder(DateElement.Year);
                day = splits[dIdx];
                month = splits[mIdx];
                year = splits[yIdx];
                try
                {
                    #region Check year
                    //syntax test
                    mYear = int.Parse(year);
                    if (mYear < 100) // the year just have two end of year digit only.
                    {
                        mYear = DateTimeHelper.CalculateYear(DateTime.Now.Year, mYear);
                    }
                    #region Defensive tracing
                    else
                    {
                        // valid year.
                    }
                    #endregion
                    if (mYear < mMinYear || mYear > mMaxYear) return false;
                    #region Defensive tracing
                    else
                    {
                        // valid value
                    }
                    #endregion
                    #endregion

                    #region check month
                    mMonth = int.Parse(month);
                    if (mMonth < 1 || mMonth > 12) return false;
                    #region Defensive tracing
                    else
                    {
                        // valid value
                    }
                    #endregion
                    #endregion

                    #region Check day
                    mDay = int.Parse(day);
                    if (mDay < 1 || mDay > GetDayOfMonth(mMonth, mYear)) return false;
                    #region Defensive tracing
                    else
                    {
                        // valid value
                    }
                    #endregion
                    #endregion
                }
                catch { return false; }
            }
            #region Defensive tracing
            else
            {
                // invalid format, return false.
                return false;
            }
            #endregion

            return true;
        }
        bool IsDigits(char ch)
        {
            foreach (char c in mDigits)
            {
                if (c.Equals(ch)) return true;
            }
            return false;
        }
        void BackWard()
        {
            if (mTextSelectionPos > 0)
            {
                if (mTextSelectionPos != 3 && mTextSelectionPos != 6) mTextSelectionPos--;
                else mTextSelectionPos -= 2;
            }
        }
        void Forward()
        {
            int t = 7;
            if (mTextSelectionPos < t)
            {
                if (mTextSelectionPos != 1 && mTextSelectionPos != 4) mTextSelectionPos++;
                else mTextSelectionPos += 2;
            }
        }
        string NumberToString(int num)
        {
            string result = string.Empty;
            if (num < 10) result = string.Format("0{0}", num);
            else if (num > 1000)
            {
                string temp = num.ToString();
                result = temp.Substring(temp.Length - 2, 2);
            }
            #region Defensive tracing
            else 
            {
                result = num.ToString();
            }
            #endregion

            return result;
        }
        void FormatDate()
        {
            if (IsValidDate(Text))
            {
                for (int i = 0; i < Text.Length; i++)
                {
                    if (IsDigits(Text[i]) == false)
                    {
                        Text = Text.Replace(Text[i].ToString(), mDateSeperator);
                    }
                }
            }
            else UpdateText();
        }
        void UpdateText()
        {
            Text = NumberToString(mYear) + mDateSeperator + NumberToString(mMonth);
            Text += mDateSeperator + NumberToString(mDay);
        }
        int GetDayOfMonth(int month, int year) 
        {
            int result = 30;

            switch (month)
            {
                case 2:
                    if (year % 4 == 0)
                    {
                        result = 29;
                    }
                    else
                    {
                        result = 28;
                    }
                    break;
                case 1:
                case 3:
                case 5:
                case 7:
                case 8:
                case 10:
                case 12:
                    result = 31;
                    break;
                case 4:
                case 6:
                case 9:
                case 11:
                    result = 30;
                    break;
                #region Defensive tracing
                default:
                    throw new Exception("Out of range.");
                #endregion
            }

            return result;
        }
        #endregion

        #endregion

        #region Constructors
        /// <summary>
        /// Default constructor.
        /// </summary>
        public DateBox()
            : base(typeof(CalendarPopupEditor))
        {
            mDefaultPopupHeight = 150;
            mDefaultPopupWidth = 187;
            Alignment = ContentAlignment.MiddleCenter;
            mIsShowPopupOnKeyPress = false;

            #region Sets default value
            DateTime now = DateTime.Now;
            Date = now;
            mYear = now.Year;
            mMonth = now.Month;
            mDay = now.Day;
            mMinYear = UI.Culture.Calendar.MinSupportedDateTime.Year;
            mMaxYear = UI.Culture.Calendar.MaxSupportedDateTime.Year;
            #endregion

            Inner.GotFocus += new EventHandler(Inner_GotFocus);
            mInnerText = new StringBuilder();
        }
        #endregion
    }
}